package org.raftkv.raft.pb.impl;


import org.raftkv.commons.Tuple;
import org.raftkv.raft.exceptions.RaftException;
import org.raftkv.raft.pb.ConfChangeSingle;
import org.raftkv.raft.pb.IConfChange;
import org.raftkv.raft.pb.enums.ConfChangeTransition;
import org.raftkv.raft.pb.enums.ConfChangeType;
import org.raftkv.raft.pb.exceptions.IntOverflowRaftException;
import org.raftkv.raft.pb.exceptions.InvalidLengthRaftException;
import org.raftkv.raft.pb.exceptions.UnexpectedEOFException;
import org.raftkv.raft.pb.utils.PbUtils;

import java.util.ArrayList;
import java.util.Arrays;

public class ConfChange implements IConfChange {

    private ConfChangeType type;

    private long nodeId;

    private byte[] context;

    private long id;

    public ConfChange() {
    }

    public ConfChange(long nodeId, ConfChangeType type) {
        this.type = type;
        this.nodeId = nodeId;
    }

    @Override
    public ConfChangeV2 asV2() {
        ConfChangeV2 ccv2 = new ConfChangeV2();

        ArrayList<ConfChangeSingle> changes = new ArrayList<>();
        changes.add(new ConfChangeSingle(this.type, this.nodeId));
        ccv2.setChanges(changes);
        ccv2.setContext(this.context);
        ccv2.setTransition(ConfChangeTransition.ConfChangeTransitionAuto);

        return ccv2;
    }

    @Override
    public Tuple<ConfChange, Boolean> asV1() {
        return new Tuple<>(this, true);
    }

    public ConfChangeType getType() {
        return type;
    }

    public void setType(ConfChangeType type) {
        this.type = type;
    }

    public long getNodeId() {
        return nodeId;
    }

    public void setNodeId(long nodeId) {
        this.nodeId = nodeId;
    }

    public byte[] getContext() {
        return context;
    }

    public void setContext(byte[] context) {
        this.context = context;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public byte[] marshal() {
        int size = this.size();
        byte[] data = new byte[size];
        int n = this.marshalToSizedBuffer(data);

        return Arrays.copyOfRange(data, 0, n);
    }

    public int size() {
        int l = 0;
        int n = 0;

        n += 1 + PbUtils.sovRaft(this.id);
        n += 1 + PbUtils.sovRaft(this.type.ordinal());
        n += 1 + PbUtils.sovRaft(this.nodeId);

        if (this.context != null) {
            l = this.context.length;
            n += 1 + l + PbUtils.sovRaft(l);
        }

        return n;
    }

    public int marshalToSizedBuffer(byte[] data) {
        int i = data.length;
        int l = 0;

        if (context != null) {
            i -= context.length;
            System.arraycopy(this.context, 0, data, i, data.length);
            i = PbUtils.encodeVarintRaft(data, i, this.context.length);
            i--;
            data[i] = 0x22;
        }
        i = PbUtils.encodeVarintRaft(data, i, this.nodeId);
        i--;
        data[i] = 0x18;
        i = PbUtils.encodeVarintRaft(data, i, this.type.ordinal());
        i--;
        data[i] = 0x10;
        i = PbUtils.encodeVarintRaft(data, i, this.id);
        i--;
        data[i] = 0x8;
        return data.length - i;
    }

    public void unmarshal(byte[] data) {
        int l = data == null ? 0 : data.length;
        int index = 0;

        while (index < l) {
            int preIndex = index;
            long wire = 0;

            for (long shift = 0; ; shift += 7) {
                if (shift >= 64) {
                    throw new IntOverflowRaftException();
                }

                if (index >= l) {
                    throw new UnexpectedEOFException();
                }

                byte b = data[index];
                index++;
                wire |= (long) (b & 0x7F) << shift;
                if (b < 0x80) {
                    break;
                }
            }

            int filedNum = (int) (wire >> 3);
            int wireType = (int) (wire & 0x7);

            if (wireType == 4) {
                String errMsg = "ConfChange: wiretype end group for non-group";
                LOGGER.error(errMsg);
                throw new RaftException(errMsg);
            }

            if (filedNum <= 0) {
                String errMsg = String.format("ConfChange: illegal tag %d (wire type %d)", filedNum, wire);
                LOGGER.error(errMsg);
                throw new RaftException(errMsg);
            }

            switch (filedNum) {
                case 1:
                    if (wireType != 0) {
                        String errMsg = String.format("wrong wireType = %d for field ID", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    this.id = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        this.id |= (long) (b & 0x7F) << shift;
                        if (b < 0x80) {
                            break;
                        }
                    }
                    break;
                case 2:
                    if (wireType != 0) {
                        String errMsg = String.format("wrong wireType = %d for field id", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    this.type = ConfChangeType.ConfChangeAddNode;
                    int tempOrdinal = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        tempOrdinal = tempOrdinal | (b & 0x7F << shift);
                        try {
                            this.type = ConfChangeType.values()[tempOrdinal];
                        } catch (IndexOutOfBoundsException ignored) {
                            ;
                        }
                        if (b < 0x80) {
                            break;
                        }
                    }
                    break;
                case 3:
                    if (wireType != 0) {
                        String errMsg = String.format("wrong wireType = %d for field nodeId", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    this.nodeId = 0L;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        this.nodeId |= (long) (b & 0x7F) << shift;
                        if (b < 0x80) {
                            break;
                        }
                    }
                    break;
                case 4:
                    if (wireType != 2) {
                        String errMsg = String.format("wrong wireType = %d for field context", wireType);
                        LOGGER.error(errMsg);
                        throw new RaftException(errMsg);
                    }
                    int byteLen = 0;
                    for (long shift = 0; ; shift += 7) {
                        if (shift >= 64) {
                            throw new IntOverflowRaftException();
                        }

                        if (index >= l) {
                            throw new UnexpectedEOFException();
                        }

                        byte b = data[index];
                        index++;
                        byteLen |= (b & 0x7F) << shift;
                        if (b < 0x80) {
                            break;
                        }
                    }
                    if (byteLen < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    int postIndex = index + byteLen;
                    if (postIndex < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    if (postIndex > l) {
                        throw new UnexpectedEOFException();
                    }
                    try {
                        this.context = Arrays.copyOfRange(data, index, postIndex);
                    } catch (Exception e) {
                        this.context = new byte[0];
                    }
                    index = postIndex;
                    break;
                default:
                    index = preIndex;
                    int skippy = PbUtils.skipRaft(Arrays.copyOf(data, index));
                    if (skippy < 0 || index + skippy < 0) {
                        throw new InvalidLengthRaftException();
                    }
                    if (index + skippy > l) {
                        throw new UnexpectedEOFException();
                    }
                    index += skippy;
            }
        }

        if (index > l) {
            throw new UnexpectedEOFException();
        }
    }


}
